﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Net;
using System.Security.Cryptography;
using System.IO;

namespace ESpace
{
    public partial class PHPService : Component
    {
        #region ** ВНУТРЕННИЕ ПЕРЕМЕННЫЕ И ТИПЫ ********************************

        class ConnectData
        {
            public string url = "localhost";
            public bool proxy = false;
            public string proxyAddr = "";
            public int proxyPort = 8080;
            public bool proxyAuten = false;
            public string proxyLogin = "";
            public string proxyPass = "";
            public string postStr = "";
            public int timeout = 5000;

            public ConnectData()
            {
                Construct(url, false, "", 0, false, "", "", "", 5000);
            }

            public ConnectData(string url, string postStr)
            {
                Construct(url, false, "", 0, false, "", "", postStr, 5000);
            }

            void Construct(string url, bool proxy, string proxyAddr, int proxyPort, bool proxyAuten, string proxyLogin, string proxyPass, string postStr, int timeout)
            {
                this.url = url;
                this.proxy = proxy;
                this.proxyAddr = proxyAddr;
                this.proxyPort = proxyPort;
                this.proxyAuten = proxyAuten;
                this.proxyLogin = proxyLogin;
                this.proxyPass = proxyPass;
                this.postStr = postStr;
                this.timeout = timeout;
            }

            public ConnectData Clone(string postStr)
            {
                ConnectData ret = new ConnectData(url, postStr);
                ret.proxy = proxy;
                ret.proxyAddr = proxyAddr;
                ret.proxyPort = proxyPort;
                ret.proxyAuten = proxyAuten;
                ret.proxyLogin = proxyLogin;
                ret.proxyPass = proxyPass;
                ret.timeout = timeout;
                return ret;
            }

        }
        ConnectData Conn = new ConnectData();

        public enum ErrorAnswer { ErrorServer, ErrorConnect, ErrorProcessing, ErrorLogin };
        enum CommResult { None, Done, Error };
        CommResult connRes; //результат коммуникаций
        List<string> PostStrList = new List<string>(); // очередь запросов
        Exception connEx = null; // ошибка коммуникаций
        object locker = new object(); // объекс для блокировки

        System.Windows.Forms.Timer tmr = new System.Windows.Forms.Timer();
        Thread th = null;

        DateTime dtTimeOut = DateTime.MaxValue; // время таймаута

        #endregion ** ВНУТРЕННИЕ ПЕРЕМЕННЫЕ И ТИПЫ ********************************

        #region *** СВОЙСТВА ******************************

        public string Url { get { return Conn.url; } set { Conn.url = value; } }
        public bool Proxy { get { return Conn.proxy; } set { Conn.proxy = value; } }
        public string ProxyAddr { get { return Conn.proxyAddr; } set { Conn.proxyAddr = value; } }
        public int ProxyPort { get { return Conn.proxyPort; } set { Conn.proxyPort = value; } }
        public bool ProxyAuten { get { return Conn.proxyAuten; } set { Conn.proxyAuten = value; } }
        public string ProxyLogin { get { return Conn.proxyLogin; } set { Conn.proxyLogin = value; } }
        public string ProxyPass { get { return Conn.proxyPass; } set { Conn.proxyPass = value; } }
        public int TimeoutConnect { get { return Conn.timeout; } set { Conn.timeout = value; } }

        int timeoutAnswer = 10000; // таймаут ожидания ответа (10000 мСек по умолчанию)
        /// <summary> Таймаут ожидания ответа (мс) </summary>
        public int TimeoutAnswer { get { return timeoutAnswer; } set { timeoutAnswer = value; } }

        string lastPostString = ""; // строка последнего пост-запроса
        public string LastPostString { get { return lastPostString; } }

        string answer = ""; // ответ
        public string LastAnswerString { get { return answer; } }

        public string Login;
        public string md5Pass;

        #endregion *** СВОЙСТВА ******************************

        #region *** СОБЫТИЯ ***************************************

        #region *** событие приема данных ***
        // класс аргументов для события
        public class RespEventArgs : EventArgs
        {
            public List<string> strL;
            public RespEventArgs(List<string> strL) { this.strL = strL; }
        }

        public delegate void OnRespDelegate(Object Sender, RespEventArgs e);
        // переменная делегата (то бишь название функции), которая будет вызываться 
        public event OnRespDelegate ResponseData;

        // генерация события приема данных
        public virtual void OnResponseData(List<string> strL)
        {
            if (ResponseData != null) ResponseData(this, new RespEventArgs(strL));
        }

        #endregion *** событие приема данных ***

        #region *** событие регистрации ***
        // класс аргументов для события
        public class RegistEventArgs : EventArgs
        {
            public bool Error;
            public string ErrorString = "";

            public RegistEventArgs(string ErrorString)
            {
                this.Error = ErrorString != "";
                this.ErrorString = ErrorString;
            }
        }

        public delegate void OnRegistDelegate(Object Sender, RegistEventArgs e);
        // переменная делегата (то бишь название функции), которая будет вызываться 
        public event OnRegistDelegate Regist;

        // генерация события приема данных
        public void OnRegist(RegistEventArgs e)
        {
            // если для события есть функция, вызываем ее
            if (Regist != null)
                Regist(this, e);
        }
        #endregion *** событие приема данных ***

        #region *** событие смены пароля ***
        // переменная делегата (то бишь название функции), которая будет вызываться 
        public event OnRegistDelegate ChangePass;

        // генерация события приема данных
        public void OnChangePass(RegistEventArgs e)
        {
            // если для события есть функция, вызываем ее
            if (ChangePass != null)
                ChangePass(this, e);
        }
        #endregion *** событие приема данных ***

        #region *** событие ошибки работы сервера или соединения ***
        // класс аргументов для события
        public class ErrEventArgs : EventArgs
        {
            public ErrorAnswer Answer;
            public string ErrorString = "";

            public ErrEventArgs(ErrorAnswer Answer, string ErrorString)
            {
                this.Answer = Answer;
                this.ErrorString = ErrorString;
            }
        }

        // делегат для события
        public delegate void OnErrorDelegate(Object Sender, ErrEventArgs e);
        // переменная делегата (то бишь название функции), которая будет вызываться 
        public event OnErrorDelegate ErrorResp;

        // генерация события приема данных
        public void OnErrorResp(ErrEventArgs e)
        {
            // если для события есть функция, вызываем ее
            if (ErrorResp != null)
                ErrorResp(this, e);
        }
        #endregion *** событие ошибки работы сервера или соединения ***

        #endregion *** СОБЫТИЯ ***************************************

        #region *** КОНСТРУКТОРЫ *******************************************

        public PHPService(IContainer container)
        {
            container.Add(this);

            tmr.Interval = 100;
            tmr.Tick += new EventHandler(Timer_Tick);
            //container.Add(tmr);

            locker = new object();

        }

        #endregion *** КОНСТРУКТОРЫ *******************************************

        #region *** ВНУТРЕННИИ ФИИ ******************************

        // таймер тик
        void Timer_Tick(object source, EventArgs e)
        {
            tmr.Stop();

            try
            {
                // если идут коммуникации, выходим
                if (th != null)
                {
                    if (th.ThreadState == ThreadState.Running)
                    {
                        if (DateTime.Now >= dtTimeOut)
                        {
                            th.Abort();
                            th = null;
                            OnErrorResp(new ErrEventArgs(ErrorAnswer.ErrorConnect, "Время ожидания ответа истекло"));
                        }
                        tmr.Start();
                        return;
                    }
                }

                // если пришел какой-то ответ после коммуникаций, делаем что-то и сбрасываем результат
                if (connRes != CommResult.None)
                {
                    if (connRes == CommResult.Error)
                    {
                        OnErrorResp(new ErrEventArgs(ErrorAnswer.ErrorConnect, connEx.Message));
                    }

                    if (connRes == CommResult.Done)
                    {
                        ParseAnswer();
                    }
                    connRes = CommResult.None;

                    PostStrList.RemoveAt(0);
                }
                else
                {
                    if (PostStrList.Count > 0)
                    {
                        answer = "";
                        connEx = null;
                        dtTimeOut = DateTime.Now.AddMilliseconds(timeoutAnswer);

                        /*answer = @"DONE~row_id;str_var;str_text;num;num_double;dt~2;строка;текст;123;123.12;2012-01-01~3;строка;текст;123;123.12;2012-01-01~4;Строка 1;Текст 1;123;123.12;2012-01-01~5;Строка \|/?$#@%^;Текст 1;123;123.12;2012-01-01~6;Строка \|/?$#@%^;Текст 1  \|/?$#@%^;123;123.12;2012-01-01~7;Строка \|/?$#@%^;Текст 1  \|/?$#@%^;123;123.12;2012-01-01~8;Строка \|/?$#@%^\;Текст 1  \|/?$#@%^;123;123.12;2012-01-01~9;'Строка \|/?$#@%^\;Текст 1  \|/?$#@%^;123;123.12;2012-01-01~10;'Строка \|/?$#@%^\&\:\\;\'\1\";
                        connRes = CommResult.Done;*/
                        th = new Thread(new ParameterizedThreadStart(StartResponse));
                        ConnectData connData = Conn.Clone(PostStrList[0]);
                        th.Start(connData);
                    }
                }

            }
            catch (Exception ex)
            {
                OnErrorResp(new ErrEventArgs(ErrorAnswer.ErrorProcessing, "Ошибка в коде: " + ex.Message + "\r\nТрассировка: " + ex.StackTrace));
                dtTimeOut = DateTime.MaxValue;
                PostStrList.RemoveAt(0);
            }

            if (PostStrList.Count > 0) tmr.Start();
            else dtTimeOut = DateTime.MaxValue;
        }

        // парсинг ответа
        void ParseAnswer()
        {
            // разбиваем на строки
            List<string> strL = GetStringList(answer, '~');
            if (strL.Count < 2) OnErrorResp(new ErrEventArgs(ErrorAnswer.ErrorServer, "Невозможно разобрать ответ: " + answer));

            // смотрим действие
            switch (strL[0])
            {
                // ошибка сервера
                case "ERROR": { OnErrorResp(new ErrEventArgs(ErrorAnswer.ErrorServer, "Ошибка на стороне сервиса: " + answer)); break; }
                // не удалось авторизоваться
                case "NOT_LOGIN": { OnErrorResp(new ErrEventArgs(ErrorAnswer.ErrorLogin, "Не удалось авторизироваться")); break; }
                // запрос выполнен успешно
                case "DONE": { OnResponseData(strL); break; }

                // при регистрации логин уже существует
                case "LOGIN_EXIST": { OnRegist(new RegistEventArgs("Такой логин уже существует")); break; }
                // регистрация пройдена успешно
                case "REGIST_OK": { OnRegist(new RegistEventArgs("")); break; }

                // смена пароля неудачна, так как не пройдена авторизация
                //case "CHANGE_PASS_NOT_LOGIN": { OnChangePass(new RegistEventArgs("Не удалось авторизоваться")); break; }
                // смена пароля успешна
                case "CHANGE_PASS_OK": { OnChangePass(new RegistEventArgs("")); break; }

                // неузвесный ответ
                default: { OnErrorResp(new ErrEventArgs(ErrorAnswer.ErrorServer, "Пришел неизвестный ответ: '" + strL[0] + "'")); break; }
            }
        }

        ushort proxyCnt = 0; // для защиты от кеша
        void StartResponse(object o)
        {
            ConnectData connData = (ConnectData)o;
            HttpWebRequest req;
            HttpWebResponse resp;

            try
            {
                string url = connData.url;
                if (connData.proxy)
                {
                    url += "&up=" + proxyCnt.ToString();
                    proxyCnt++;
                }
                req = (HttpWebRequest)WebRequest.Create(url);

                System.Net.Cache.HttpRequestCachePolicy noCachePolicy = new System.Net.Cache.HttpRequestCachePolicy(System.Net.Cache.HttpRequestCacheLevel.NoCacheNoStore);
                req.CachePolicy = noCachePolicy;
                if (connData.proxy)
                {
                    WebProxy myProxy = new WebProxy(connData.proxyAddr, connData.proxyPort);
                    //myProxy.BypassProxyOnLocal = true;
                    if (connData.proxyAuten)
                        myProxy.Credentials = new NetworkCredential(connData.proxyLogin, connData.proxyPass);
                    req.Proxy = myProxy;
                    //req.Proxy.Credentials = CredentialCache.DefaultCredentials;
                    //req.PreAuthenticate = true;
                }
                else
                {
                    req.Proxy = null;
                }

                req.Method = "POST"; // Устанавливаем метод передачи данных в POST
                req.Timeout = connData.timeout; // Устанавливаем таймаут соединения
                req.ContentType = "application/x-www-form-urlencoded"; // указываем тип контента
                // передаем список пар параметров / значений для запрашиваемого скрипта методом POST
                byte[] sentData = Encoding.UTF8.GetBytes(connData.postStr);
                req.ContentLength = sentData.Length;
                System.IO.Stream sendStream = req.GetRequestStream();
                sendStream.Write(sentData, 0, sentData.Length);
                sendStream.Close();
            }
            catch (Exception ex)
            {
                lock (locker)
                {
                    connEx = ex;
                    connRes = CommResult.Error;
                    return;
                }
            }


            try
            {
                resp = (HttpWebResponse)req.GetResponse();

                StreamReader rdr = new StreamReader(resp.GetResponseStream(), Encoding.UTF8);
                string ans = rdr.ReadToEnd();
                resp.Close();


                lock (locker)
                {
                    answer = ans;
                    connRes = CommResult.Done;
                }
            }
            catch (Exception ex)
            {
                lock (locker)
                {
                    connEx = ex;
                    connRes = CommResult.Error;
                    return;
                }
            }

        }

        #endregion *** ВНУТРЕННИИ ФИИ ******************************

        #region *** ЗАПРОСЫ **************************************

        public void RequestData(string PostData)
        {
            PostStrList.Add("l=" + Login + "&p=" + md5Pass + PostData);
            lastPostString = "l=" + Login + "&p=" + md5Pass + PostData;
            tmr.Start();
        }

        public void RequestRegistration(string login, string md5Pass)
        {
            PostStrList.Add("l=" + login + "&pn=" + md5Pass + "&reg=1");
            lastPostString = "l=" + login + "&pn=" + md5Pass + "&reg=1";
            tmr.Start();
        }

        public void RequestChangePass(string md5NewPass)
        {
            PostStrList.Add("l=" + Login + "&p=" + md5Pass + "&pn=" + md5NewPass + "&reg=2");
            lastPostString = "l=" + Login + "&p=" + md5Pass + "&pn=" + md5NewPass + "&reg=2";
            tmr.Start();
        }

        #endregion *** ЗАПРОСЫ **************************************

        public new void Dispose()
        {
            if (th != null) if (th.ThreadState == ThreadState.Running) { th.Abort(); }
            tmr.Dispose();
        }

        public static String GetMD5(String s)
        {
            byte[] hash = new MD5CryptoServiceProvider().ComputeHash(Encoding.UTF8.GetBytes(s));
            // конвертирование в строку
            StringBuilder sb = new StringBuilder(32);
            foreach (byte hex in hash)
                sb.Append(hex.ToString("X2"));
            return sb.ToString().ToLower();
        }

        public static String GetMySqlDateTime(DateTime dt) { return dt.ToString("yyyy-MM-dd HH:mm:ss"); }

        /// <summary>Разделяем строку на подстроки используя спец-символ, как разделитель. При этом заменяем обрамленные символы (которые обрамлялись, чтобы не спутать со спец-символом) на обычные</summary>
        public static List<string> GetStringList(string str, char splitChar)
        {
            List<string> strL = new List<string>();
            int searchInd = 0; // индекс, откуда начинать поиск следующего символа
            int addInd = 0; // индек символа, с какого добавлять следующую строку
            while (true)
            {
                // если сделующий индекс для поиска уже вышел за строку, выходим
                if (searchInd > str.Length) break;

                // находим позицию следующего символа
                int newInd = str.IndexOf(splitChar, searchInd);
                if (newInd == -1) break; // если потенциальных спец-символов больше не найдено, выходим
                // смотрим количество обаратных слешей перед символом
                int cnt = 0; for (int i = (newInd - 1); i >= searchInd; i--) { if (str[i] != '\\') break; cnt++; }
                // если количество четное (или равно 0), значит это спец символ (добавляем подстроку)
                if (cnt % 2 == 0)
                {
                    // придобавлении заменяем обрамленные символы (которые обрамлялись, чтобы не спутать со спец-символом) на обычные
                    strL.Add(str.Substring(addInd, newInd - addInd).Replace("\\" + splitChar, splitChar.ToString()));
                    addInd = newInd + 1;
                }
                searchInd = newInd + 1;
            }

            // добавляем последнюю строку, даже, если она нулевая
            if (addInd >= str.Length) strL.Add("");
            else strL.Add(str.Substring(addInd, str.Length - addInd).Replace("\\" + splitChar, splitChar.ToString()));

            return strL;
        }

        /// <summary> Удаление экранирующего символа '\' Можно делать только в том случае, когда все разбиения на строки и подстроки методом GetStringList уже сделаны </summary>
        public static void DeleteEscapeCharacters(string[] StrL)
        {
            for (int i = 0; i < StrL.Length; i++) StrL[i] = StrL[i].ToUpper().Replace("\\\\", "\\");
        }

        /// <summary> Удаление экранирующего символа '\' Можно делать только в том случае, когда все разбиения на строки и подстроки методом GetStringList уже сделаны </summary>
        public static void DeleteEscapeCharacters(List<string> StrL)
        {
            for (int i = 0; i < StrL.Count; i++) StrL[i] = StrL[i].Replace("\\\\", "\\");
        }

        public static string GetPostParam(string ParamName, string ValueString)
        {
            return "&" + ParamName + "=" + ValueString.Replace("%", "%25").Replace("&", "%26").Replace("+", "%2B").Replace("?", "%3f"); //.Replace(".", "%2e");
            // спец-символы ( на всякий). Статья http://ru.wikipedia.org/wiki/URL
            /* ! 	" 	#[4] 	 % 	&[4] 	' 	* 	,[4] 	 :[4] 	 ;[4] 	< 	=[4] 	> 	 ?[4] 	[ 	] 	^ 	` 	{ 	| 	} 	<пробел>
              %21 	 %22 	 %23 	 %25 	 %26 	 %27 	 %2a 	 %2c 	 %3a 	 %3b 	 %3c 	 %3d 	 %3e 	 %3f 	 %5b 	 %5d 	 %5e 	 %60 	 %7b 	 %7c 	 %7d 	 %20[5]*/
        }

        /// <summary>Класс работы с сервисом свободного sql общения</summary>
        public class FreeSql
        {
            /// <summary> парсит пришедшие в ответе данные в виде таблицы, которую
            /// таблица - это соллекция строк 
            /// каждая строка - это соллекция пар "ключ -> значение" 
            /// ключи - это названия столбцов
            /// значения - значения ячеек
            /// </summary>
            public static List<Dictionary<string, string>> ParseRequestString(List<string> strL, out string sqlErr)
            {
                sqlErr = "";
                List<Dictionary<string, string>> ret = new List<Dictionary<string, string>>();

                // смотрим первую строку
                if (strL[1] == "DONE")
                {
                    sqlErr = "";
                }
                else
                {
                    // ошибка sql_запроса клиента
                    if (strL[1] == "ERROR_SQL") sqlErr = "Ошибка в SQL-запросе: " + strL[2];
                    // неизвестная
                    else sqlErr = "Неизвестный ответ: " + strL[1];
                    return ret;
                }

                if (strL[2] == "NOT_DATA") return ret;
                if (strL[2] == "") { return null; }

                // разбиваем на лексемы строку названий полей
                List<string> key = GetStringList(strL[2], ';');
                DeleteEscapeCharacters(key); //for (int i = 0; i < key.Count; i++) key[i] = key[i].ToUpper().Replace("\\\\", "\\");
                for (int i = 0; i < key.Count; i++) key[i] = key[i].ToUpper();

                // заполняем остальное
                for (int i = 3; i < strL.Count; i++)
                {
                    Dictionary<string, string> row = new Dictionary<string, string>();
                    // разбиваем на лексемы очередную строку
                    List<string> sl = GetStringList(strL[i], ';');
                    if (sl.Count != key.Count) throw new Exception("размер массива названий полей (" + key.Count + ") и размер массива значений (" + sl.Count + ") не совпадают. Строка " + (i - 3).ToString());
                    DeleteEscapeCharacters(sl); //for (int j = 0; j < sl.Count; j++) row.Add(key[j], sl[j].Replace("\\\\", "\\"));
                    ret.Add(row);
                }

                return ret;
            }

            /// <summary> Создать свободный запрос на сервер
            /// Возвращает строку для PostData
            /// В запросе можно указывать параметры как ?0 ?1 ?2 ... параметры в запросе должны начинаться с нуля и далее +1 без пропусков
            /// параметры могут стоять вразнобой и повторятся любое количество раз
            /// </summary>
            /// <param name="CommantText">текст запроса.</param>
            /// <param name="Params">список значение параметров в виде любых строк</param>
            /// <param name="GetAnswer">возвращать ли ответ в виде таблицы</param>
            public static string GetPostData(string CommantText, List<string> Params, bool GetAnswer)
            {
                StringBuilder pStr = new StringBuilder();
                pStr.Append(PHPService.GetPostParam("sql", CommantText));
                if (GetAnswer) pStr.Append("&rdr=1");
                if (Params != null)
                {
                    pStr.Append("&prmLen=" + Params.Count.ToString());
                    for (int i = 0; i < Params.Count; i++) pStr.Append(PHPService.GetPostParam("prm" + i.ToString(), Params[i]));
                }

                return pStr.ToString();
            }

            /// <summary> 
            /// 
            /// В запросе можно указывать параметры как ?0 ?1 ?2 ... параметры в запросе должны начинаться с нуля и далее +1 без пропусков
            /// параметры могут стоять вразнобой и повторятся любое количество раз
            /// </summary>
            /// <param name="CommantText">текст запроса.</param>
            /// <param name="Params">список значение параметров в виде любых строк</param>
            /// <param name="GetAnswer">возвращать ли ответ в виде таблицы</param>

            /// <summary>
            /// Создать свободный запрос вставки строки на сервер
            /// Возвращает строку для PostData
            /// </summary>
            /// <param name="TblName">Имя таблицы</param>
            /// <param name="Fields">Коллекция полей ключ-значение. Ключ - название поля в БД. Значение - значение поля</param>
            public static string GetPostData_InsertRow(string TblName, Dictionary<string, string> Fields)
            {
                if (Fields == null) throw new Exception("Нет полей для вставки в таблицу");
                if (Fields.Count == 0) throw new Exception("Нет полей для вставки в таблицу");

                List<string> Params = new List<string>();
                StringBuilder CommantText = new StringBuilder();
                StringBuilder vals = new StringBuilder();
                CommantText.Append("insert into " + TblName + " (");
                int cnt = 0;
                foreach (KeyValuePair<string, string> fld in Fields)
                {
                    if (cnt > 0)
                    {
                        CommantText.Append(", ");
                        vals.Append(", ");
                    }
                    CommantText.Append(fld.Key);
                    vals.Append("?" + cnt.ToString());
                    Params.Add(fld.Value);
                    cnt++;
                }

                CommantText.Append(") values(");
                CommantText.Append(vals.ToString());
                CommantText.Append(")");

                return GetPostData(CommantText.ToString(), Params, false);

            }

            /// <summary>
            /// Создать свободный запрос Update строки на сервер
            /// Возвращает строку для PostData
            /// </summary>
            /// <param name="TblName">Имя таблицы</param>
            /// <param name="Fields">Коллекция полей ключ-значение. Ключ - название поля в БД. Значение - значение поля</param>
            /// <param name="SQLAfterWhere">Часть запроса после слова where. Если условия не нужны, оставить строку пустой</param>
            public static string GetPostData_UpdateRow(string TblName, Dictionary<string, string> Fields, string SQLAfterWhere)
            {
                if (Fields == null) throw new Exception("Нет полей для вставки в таблицу");
                if (Fields.Count == 0) throw new Exception("Нет полей для вставки в таблицу");

                List<string> Params = new List<string>();
                StringBuilder CommantText = new StringBuilder();
                StringBuilder vals = new StringBuilder();
                CommantText.Append("update " + TblName + " set ");
                int cnt = 0;
                foreach (KeyValuePair<string, string> fld in Fields)
                {
                    if (cnt > 0) CommantText.Append(", ");
                    CommantText.Append(fld.Key + "=?" + cnt.ToString());
                    Params.Add(fld.Value);
                    cnt++;
                }

                if (SQLAfterWhere.Trim() != "") CommantText.Append(" where " + SQLAfterWhere);

                return GetPostData(CommantText.ToString(), Params, false);
            }

        }

    }
}
